8장 일부만 바꾸고 일부만 가져오기 (입문판)

원서: API Design Patterns (Manning, JJ Geewax) 8장을 입문자용으로 풀어 씀.

이 장에서 딱 6가지만 가져가요. 1. 통째로 주고받는 게 왜 위험한지 설명한다. 2. 필드 마스크가 뭔지 설명한다. 3. 부분 취득과 부분 업데이트를 구분한다. 4. 필드 경로 표기 규칙(점, 별표, 백틱)을 설명한다. 5. 배열은 왜 통째로만 바꾸는지 설명한다. 6. 안 보냈을 때 어떻게 알아서 처리되는지 설명한다.


들어가며 — 토핑 하나 바꾸려고 피자를 다시 시킨 적 있죠?

이런 적 있죠. 주소 하나 바꾸려고 회원정보 수정 화면에 들어갔어요. 이름, 전화번호, 주소, 이메일이 다 떠 있어요. 주소만 고치고 저장을 눌렀어요.

그런데 그 사이 다른 기기에서 전화번호를 바꿔놨다면? 내가 누른 저장이 옛날 전화번호로 도로 덮어써 버려요. 주소만 바꾸려다 전화번호를 망친 거예요.

이게 바로 이 장이 풀려는 문제예요. "하나만 바꾸고 싶은데 왜 전부를 다시 보내야 하지?"

이 장은 그 답으로 필드 마스크라는 도구를 줍니다. "이것만 줘", "이것만 바꿔" 라고 콕 집어 말하는 방법이에요.


8.1 통째로 주고받기 — 왜 터지는가?

먼저 사고 장면부터

피자집에 전화했어요. "페퍼로니 빼고 버섯으로 바꿔주세요."

그런데 가게 규칙이 이래요. "토핑 하나 바꾸려면 피자를 처음부터 다시 주문하셔야 해요. 도우, 소스, 치즈, 토핑 전부 다시 말씀해 주세요."

황당하죠? 그게 바로 전체 교체 방식이에요.

전체 교체(replace): 필드 하나를 바꾸려고 리소스 전체를 다시 보내는 방식. 일상비유로, 메모 한 줄 고치려고 노트 전체를 새로 베껴 쓰는 것과 같아요. (참고: '리소스'가 낯설면 0장 용어집을 보세요. 여기선 '서버에 저장된 데이터 한 덩어리', 예를 들면 '채팅방 하나'라고 생각하면 돼요.)

이 방식은 세 군데서 터져요. 하나씩 볼게요.

문제 1 — 데이터 낭비

채팅방 정보가 이렇게 크다고 해봐요. 제목, 설명, 설정, 로그 설정, 관리자 목록... 제목 한 글자 바꾸려고 이걸 통째로 매번 보내요.

작은 IoT 기기(예: 온도 센서)나 비싼 모바일 데이터 환경에선 이게 부담이에요. 한 글자 보내려고 트럭을 부르는 셈이죠.

문제 2 — 동시 수정 충돌 (데이터 유실)

이게 제일 무서운 문제예요.

두 사람이 같은 채팅방을 동시에 손봐요. 사용자 1: "제목을 '내 방'으로 바꿀래" 사용자 2: "설명을 '멋진 방'으로 바꿀래"

둘 다 전체 교체로 보내면 어떻게 될까요? 나중에 도착한 사람이 먼저 도착한 사람을 덮어써요. 사용자 1의 제목 변경이 통째로 사라질 수 있어요.

들어가며에서 봤던 '전화번호 사고'가 바로 이거예요.

문제 3 — 새 필드가 추가되면 유실

API가 자라면서 description(설명) 필드가 새로 생겼어요. 그런데 오래된 앱은 이 필드를 아직 몰라요.

옛날 앱이 제목만 바꾸려고 전체 교체를 보내요. 설명 필드를 모르니까 그 자리를 비워서 보내요. 서버는 이렇게 해석해요. "설명을 비웠네? 지우라는 뜻이구나!"

멀쩡하던 설명이 사라져요. 바꾼 적도 없는데요.

비유-코드-위험 한눈에

비유 API/코드 위험·주의
토핑 하나 바꾸려고 피자 통째 재주문 PUT /chatRooms/1 에 전체 리소스 데이터 낭비 (작은 기기·비싼 데이터에 부담)
두 사람이 같은 메모를 동시에 덮어씀 두 명이 전체 교체를 동시에 보냄 나중 사람이 먼저 사람 변경을 덮어 유실
옛날 앱이 새 칸을 몰라서 빈칸으로 보냄 구버전이 description 빼고 전체 교체 서버가 "지워라"로 오해 → 데이터 유실

잘못된 예 vs 올바른 예

// 잘못된 예 — 제목만 바꾸고 싶은데 전체를 보냄
let room = GetChatRoom({ id: "1" });
// room = { id:"1", title:"옛 제목", description:"멋진 방", settings:{...} }
room.title = "새 제목";
ReplaceChatRoom({ chatRoom: room });
// 다른 사람이 바꾼 description까지 도로 덮어쓸 위험!
// 올바른 예 — 제목만 콕 집어서 바꿈
UpdateChatRoom({
  resource: { title: "새 제목" },
  fieldMask: ["title"]   // "제목만 바꿀 거야!"
});
// description, settings는 손도 안 댐

핵심 한 줄. 전체 교체는 낭비 + 충돌 + 유실, 세 가지 위험이 있어요.


8.2 필드 마스크 — "이것만"이라고 콕 집기

먼저 장면부터

마트에 가서 이렇게 말하면 어떻게 될까요? "여기 있는 물건 전부 보여주세요." 물건이 10만 개면 트럭이 필요해요.

이렇게 말하면요? "우유랑 빵만 보여주세요." 가방 하나면 충분해요.

이 "우유랑 빵만" 이 바로 필드 마스크예요.

필드 마스크(Field Mask): "이 필드들만 다뤄줘" 라고 서버에 건네는 필드 이름 목록(문자열 배열). 일상비유로, 장바구니 메모 같은 거예요. 적힌 것만 담아요. (참고: '필드'는 데이터 한 칸이에요. 채팅방의 '제목 칸', '설명 칸' 같은 거. 0장 용어집 참고.)

생긴 건 그냥 이름들의 줄 세우기예요.

["title", "description"]

"제목 칸이랑 설명 칸, 이 둘만." 이런 뜻이에요.

두 가지 쓰임 — 받을 때와 바꿀 때

필드 마스크는 두 자리에서 써요.

부분 취득 — 받을 때 "이것만 줘"

부분 취득(Partial Get): 리소스에서 원하는 필드만 골라서 가져오기.

GET /chatRooms/1                  → 전부 다 줘 (마스크 없음)
GET /chatRooms/1  fieldMask=title → 제목만 줘 (가볍다!)

부분 업데이트 — 바꿀 때 "이것만 바꿔"

부분 업데이트(Partial Update): 리소스에서 원하는 필드만 골라서 수정하기.

PUT   /chatRooms/1  { 전체 리소스 }            → 통째로 교체 (위험)
PATCH /chatRooms/1?fieldMask=title  { title:"새!" } → 제목만 바뀌고 나머지 그대로

비유-코드-위험 한눈에

비유 API/코드 위험·주의
마트에서 "우유랑 빵만 주세요" fieldMask=["우유","빵"] 마스크 안 적으면 전부 와서 무거움
장바구니 메모에 적은 것만 담기 부분 취득 GET ...?fieldMask=title 받을 때는 GET에 마스크를 붙임
메모 한 줄만 고치기 부분 업데이트 PATCH ...?fieldMask=title 바꿀 때는 PATCH에 마스크를 붙임

최소 코드 예 — 그냥 문자열 배열이에요

type FieldMask = string[];   // 그냥 문자열 목록

interface GetChatRoomRequest {
  id: string;
  fieldMask: FieldMask;   // ["title"] 처럼 받고 싶은 칸만
}

interface UpdateChatRoomRequest {
  resource: ChatRoom;     // 바꿀 데이터
  fieldMask: FieldMask;   // ["title"] 처럼 바꿀 칸만
}

핵심 한 줄. 필드 마스크는 "다룰 칸 이름들"의 목록이고, 받을 때(GET)도 바꿀 때(PATCH)도 쓴다.


8.3 어떻게 만들까

이제 진짜 구현이에요. 한 걸음씩, 개념 하나에 섹션 하나로 갑니다.


8.3.1 마스크를 어디에 실어 보낼까

먼저 장면부터

편지를 보내요. 받는 사람 주소를 어디에 쓸까요? 봉투 겉면에 쓰죠. 봉투 안쪽에 숨겨 쓰면 우체부가 못 봐요.

필드 마스크도 똑같아요. 잘 보이는 자리에 실어야 해요.

표준 규칙(기본 안전판): 필드 마스크는 쿼리 매개변수로 보낸다. 쿼리 매개변수는 URL 끝에 ?로 붙는 부분이에요. 봉투 겉면처럼 눈에 보여요. (참고: 0장 용어집 — 쿼리 매개변수)

GET /chatRooms/1?fieldMask=title&fieldMask=description
                 └────────── 봉투 겉면 (URL에 그대로 보임) ──────────┘

왜 본문(body)에 못 넣나

방법 가능 여부 이유
쿼리 매개변수 추천 URL에 보여서 디버깅 쉬움
HTTP 헤더 가능하지만 비추천 숨어 있어서 눈으로 확인 어려움
요청 본문(body) 안 됨 GET은 본문 없음(HTTP 규칙), PATCH 본문은 리소스 데이터 전용

GET 요청은 본문을 가질 수 없어요. PATCH 본문은 "바꿀 데이터"가 들어가는 자리라 마스크가 끼어들 틈이 없어요. 그래서 쿼리 매개변수가 정답이에요.

프레임워크마다 다중 값 모양이 조금 다름

같은 마스크라도 도구마다 모양이 살짝 달라요.

프레임워크 형식 비고
PHP ?fieldMask[]=a&fieldMask[]=b 대괄호 붙음
Flask (Python) ?fieldMask=a&fieldMask=b request.args.to_dict(flat=False)
Spring (Java) ?fieldMask=a&fieldMask=b
Express (Node.js) ?fieldMask=a&fieldMask=b

직접 문자열을 자르지 말고, 프레임워크의 표준 라이브러리를 쓰세요.

비유-코드-위험 한눈에

비유 API/코드 위험·주의
주소는 봉투 겉면에 마스크는 쿼리 매개변수에 본문(body)에 넣으면 GET은 아예 안 됨
봉투 안쪽에 숨기면 못 봄 HTTP 헤더에 넣기 숨어서 디버깅 어려움

최소 코드 예 — 받을 때 (worked example: 다 보여줌)

# Flask 부분 취득 — 요청한 칸만 골라 반환
from flask import request

@app.route("/chatRooms/<id>", methods=["GET"])
def get_chat_room(id):
    # URL: /chatRooms/1?fieldMask=title&fieldMask=description
    field_mask = request.args.getlist("fieldMask")  # ['title','description']
    room = db.get_chat_room(id)

    if field_mask:
        result = {k: v for k, v in room.items() if k in field_mask}
        result["id"] = room["id"]   # id는 항상 포함
    else:
        result = room               # 마스크 없으면 전부
    return jsonify(result)

부분 완성 — 바꿀 때 (빈칸을 채워보세요)

# Flask 부분 업데이트 — 마스크에 적힌 칸만 수정
@app.route("/chatRooms/<id>", methods=["PATCH"])
def update_chat_room(id):
    field_mask = request.args.getlist("fieldMask")
    data = request.get_json()
    room = db.get_chat_room(id)

    for field in field_mask:
        if field in data:
            room[____] = data[____]   # 빈칸: 둘 다 field

    db.save_chat_room(room)
    return jsonify(room)

핵심 한 줄. 마스크는 URL의 쿼리 매개변수로 보낸다. 본문에는 못 넣는다.


8.3.2 중첩 구조와 맵 — 깊은 칸을 가리키는 법

먼저 장면부터

택배를 보내는데 주소가 이렇게만 적혀 있으면 어떻게 될까요? "행복아파트" 몇 동 몇 호인지 없으면 못 찾죠.

데이터도 칸 안에 칸이 있는 경우가 많아요. 그 안쪽 칸을 가리키려면 "몇 동 몇 호" 같은 경로가 필요해요.

표준 규칙(기본 안전판): 점(.)으로 안쪽 경로를 잇고, 별표(*)로 "그 아래 전부"를 가리킨다.

"title"                    → 1층 거실 (단순 칸)
"loggingConfig.maxSizeMb"  → 3동 > 302호 > 거실 (점으로 안쪽 경로)
"loggingConfig.*"          → 3동 전체 (별표 = 그 아래 전부)

실제 데이터로 보기

interface ChatRoom {
  id: string;
  title: string;
  description: string;
  loggingConfig: LoggingConfig;  // 칸 안의 칸 (중첩)
  settings: Object;              // 키-값 보관함 (맵)
}

interface LoggingConfig {
  maxSizeMb: number;
  maxMessageCount: number;
}

중첩 인터페이스(Nested Interface): 리소스 칸 안에 또 들어 있는 구조 한 덩어리. 일상비유로, 서랍장 안의 작은 칸막이 같은 거예요.

맵 필드(Map Field): 이름표를 붙여 물건을 넣는 보관함. 칸 개수가 고정이 아니라 자유로워요. 일상비유로, 번호표 사물함이에요. 1234번 칸, hello번 칸처럼 키로 찾아요.

한 걸음 더 ▸ 맵 키에 특수문자가 있을 때 (백틱)

한 걸음 더 ▸ 여기는 예외 처리예요. 처음엔 점(.)과 별표(*)만 익혀도 충분해요.

맵의 키 이름에 점(.)이나 별표(*) 같은 특수문자가 들어가면 헷갈려요. 예를 들어 키가 "test.value"인데 그냥 settings.test.value라고 쓰면 서버가 "settings 안의 test 안의 value"로 오해해요.

그래서 키를 백틱(`)으로 감싸요. "이건 통째로 키 이름이야" 라는 표시예요.

settings.`1234` → settings 맵의 "1234" 키 settings.`test.value` → settings 맵의 "test.value" 키 (점 포함, 통째로 키) settings.`*` → settings 맵의 "*" 키 (별표 포함, 통째로 키)

백틱 자체가 키에 들어가면 백틱을 두 번 연달아(``) 써서 표시해요. 예: 키가 "helo"이면 → ``settings.helo`

잘못된 예 vs 올바른 예

// 키 이름이 "hello.world" 인 칸을 가리키고 싶을 때

// 잘못된 예
fieldMask=settings.hello.world
→ settings 안의 hello 안의 world (3단계로 오해!)

// 올바른 예
fieldMask=settings.`hello.world`
→ settings 맵의 "hello.world" 키 (점 포함, 통째로 한 키)

비유-코드-위험 한눈에

비유 API/코드 위험·주의
행복아파트 3동 302호 거실 loggingConfig.maxSizeMb 점을 빼면 안쪽 칸을 못 가리킴
3동 전체 loggingConfig.* 별표는 "그 아래 전부"
번호표 사물함 1234번 칸 settings.\1234`` 특수 키는 백틱으로 감싸야 안 깨짐

핵심 한 줄. 안쪽 칸은 점으로 잇고, 그 아래 전부는 별표, 특수한 맵 키는 백틱으로 감싼다.


8.3.3 배열(반복 필드) — 왜 통째로만 바꾸나

먼저 장면부터

놀이공원 줄을 떠올려봐요.

줄: [철수, 영희, 민수]
     0번   1번   2번

"1번 사람 이름을 바꿔줘" 라고 부탁했어요. 그런데 그 사이 누가 새치기를 했어요.

[철수, 지수, 영희, 민수]
 0번   1번   2번   2번... → 이제 1번은 지수예요!

영희를 바꾸려다 엉뚱한 지수를 바꾸게 돼요. 번호(인덱스)는 줄이 바뀌면 가리키는 사람이 달라져요. 믿을 수 없는 이름표예요.

반복 필드(Repeated Field): 같은 모양의 데이터를 줄 세운 것. 쉽게 말해 배열이에요. 일상비유로, 줄 서 있는 사람들이에요.

표준 규칙(기본 안전판): 배열은 번호로 한 칸만 콕 집어 바꾸지 않는다. 통째로 갈아끼운다.

읽기는 별표로 OK, 쓰기는 통째로만

interface ChatRoom {
  id: string;
  title: string;
  administrators: User[];   // 관리자 줄 (배열)
}
interface User { name: string; email: string; }
// 읽기는 가능 — 모든 관리자의 이름만 가져오기
GET /chatRooms/1?fieldMask=administrators.*.name
→ "각 관리자에 대해(*), 이름(name)만 줘"
// 쓰기는 통째 교체만
PATCH /chatRooms/1?fieldMask=administrators
{
  "administrators": [
    { "name": "Alice", "email": "[email protected]" },
    { "name": "Bob",   "email": "[email protected]" }
  ]
}
→ 줄 전체를 새 줄로 갈아끼움

잘못된 예 vs 올바른 예

// 잘못된 예 — 번호로 한 명만 바꾸려는 시도
PATCH /chatRooms/1?fieldMask=administrators.*.name
{
  "administrators": [
    { "name": "0번 새 이름" },
    { "name": "1번 새 이름" }
  ]
}
→ 줄 순서가 바뀌었으면 엉뚱한 사람 이름이 바뀜!

// 올바른 예 — 줄 전체를 통째로 교체
PATCH /chatRooms/1?fieldMask=administrators
{ "administrators": [ ...새 전체 목록... ] }

한 걸음 더 ▸ 정말 한 명만 바꾸고 싶다면

한 걸음 더 ▸ 개별 수정이 꼭 필요하면 줄(배열) 대신 둘 중 하나를 써요. 1. 맵으로 저장 — 키가 안정적인 이름표라서 "Alice 칸"을 콕 집을 수 있어요. 2. 서브리소스로 분리 — 각 관리자에게 고유 주소(ID)를 줘서 따로 다뤄요. (자세한 건 8.5 연습문제 1번에서 다시 봐요.)

비유-코드-위험 한눈에

비유 API/코드 위험·주의
줄 선 사람들 administrators: User[] 번호는 줄 바뀌면 가리키는 사람이 달라짐
줄 전체 다시 세우기 fieldMask=administrators 통째 교체 쓰기는 통째로만 안전
각자 이름표만 읽기 administrators.*.name 읽기 읽기는 별표로 OK, 쓰기엔 쓰지 말 것

핵심 한 줄. 배열의 번호는 불안정하다. 읽기는 별표로 되지만, 바꿀 땐 통째로 갈아끼운다.


8.3.4 마스크를 안 보내면 어떻게 되나 (기본값)

먼저 장면부터

식당에서 "메뉴 보여주세요" 라고만 하면? 전체 메뉴판을 줘요. (받을 때 기본 = 전부)

"이 피자 토핑을 버섯으로요" 라고 하면? 가게가 알아서 "아, 토핑만 바꾸면 되네" 하고 알아채요. (바꿀 때 기본 = 보낸 내용으로 짐작)

표준 규칙(기본 안전판): 받을 때(GET) 마스크가 없으면 → 전부 준다. 바꿀 때(PATCH) 마스크가 없으면 → 보낸 데이터를 보고 짐작한다(8.3.5에서 자세히).

별표 "*" — "지금 있는 거 전부" 라는 약속어

fieldMask=*   → "모든 칸을 줘"

센티넬 값(Sentinel Value): "전부" 같은 특별한 뜻을 가진 약속된 값. 일상비유로, 식당의 "오늘의 전체 코스 주세요" 같은 한마디예요.

별표가 유용한 이유. 내 앱이 마지막으로 본 뒤 서버에 새 칸이 생겼을 수 있어요. 칸 이름을 하나하나 적으면 새 칸을 놓쳐요. *를 쓰면 "지금 있는 거 전부"라서 새 칸도 자동으로 포함돼요.

한 걸음 더 ▸ 일부러 빼놓는 무거운 칸

한 걸음 더 ▸ 보통은 마스크가 없으면 전부 주지만, 예외가 있어요.

계산에 오래 걸리거나 덩치가 큰 칸은 기본에서 빼둘 수 있어요. 예를 들어 analytics(통계) 칸을 만드는 데 3분이 걸린다면? 매번 주면 너무 느려요. 그래서 기본에서 제외해요. 사용자가 fieldMask=analytics라고 콕 집어 요청할 때만 줘요. 단, 이런 칸은 반드시 API 문서에 "기본에서 빠진다"고 적어둬야 해요.

그래서 기본 동작(마스크 없음)과 *는 미묘하게 달라요. *는 이렇게 빼둔 무거운 칸까지 포함할 수 있어요.

비유-코드-위험 한눈에

비유 API/코드 위험·주의
"메뉴 보여주세요" → 전체 메뉴판 GET 마스크 없음 → 전부 큰 리소스면 무거울 수 있음
"오늘의 전체 코스" 한마디 fieldMask=* 새로 생긴 칸도 자동 포함
만드는 데 3분 걸리는 특선 무거운 analytics 기본에서 빼고 문서에 명시

핵심 한 줄. 받을 때 기본은 전부, 바꿀 때 기본은 짐작. *는 "지금 있는 전부"라는 약속어.


8.3.5 묵시적 필드 마스크 — 안 적었으면 알아서 짐작

먼저 장면부터

택배를 보내는데 "내용물 목록"을 안 적었어요. 기사님이 상자를 열어봐요. "아, 책이랑 펜이 들어 있네. 이것만 전달하면 되겠다."

PATCH에서 마스크를 안 보내면 서버도 똑같이 해요. 보낸 JSON을 열어보고 "title이랑 description이 있네, 이것만 바꾸면 되겠다" 하고 짐작해요.

묵시적 필드 마스크(Implicit Field Mask): 마스크를 따로 안 보냈을 때, 보낸 데이터의 키를 보고 자동으로 만들어내는 마스크. 일상비유로, 목록 안 적은 택배의 내용물을 기사님이 열어보고 파악하는 것.

짐작하는 방법 (inferFieldMask)

function inferFieldMask(resource: Object): string[] {
  const mask: string[] = [];
  for (const [key, value] of Object.entries(resource)) {
    if (value instanceof Object) {
      // 안에 또 칸이 있으면 안쪽까지 파고들어 점으로 이어붙임
      for (const field of inferFieldMask(value)) {
        mask.push(`${key}.${field}`);
      }
    } else if (value !== undefined) {
      // 값이 들어 있으면 → 이 칸을 바꾸겠다는 뜻
      mask.push(key);
    }
    // 값이 비어(undefined) 있으면 → 건드리지 않음
  }
  return mask;
}

명시적 vs 묵시적 — 같은 결과, 다른 방식

// 명시적 — 내가 직접 마스크를 적음
fetch("/chatRooms/1?fieldMask=title", {
  method: "PATCH",
  body: JSON.stringify({ title: "새 제목" })
});
// 서버: "title만 바꾸면 되는구나" (내가 알려줌)

// 묵시적 — 마스크 없이 데이터만 보냄
fetch("/chatRooms/1", {
  method: "PATCH",
  body: JSON.stringify({ title: "새 제목" })
});
// 서버: JSON을 열어보고 "title만 있네, 이것만" (알아서 짐작)

흐름으로 보기

보낸 JSON:  { "title":"새!", "description":"멋짐" }
       ↓ inferFieldMask()
짐작한 마스크: ["title", "description"]
       ↓ 이 칸만 수정
title → "새!" (변경)   description → "멋짐" (변경)
settings, loggingConfig → 그대로 (마스크에 없으니 안 건드림)

비유-코드-위험 한눈에

비유 API/코드 위험·주의
목록 없는 택배를 열어 파악 마스크 없이 PATCH 짐작이라 가끔 애매함(특히 null)
내용물 목록을 직접 적음 ?fieldMask=title 명시 더 정확함, 매번 적어야 함

핵심 한 줄. 마스크를 안 보내면 서버가 보낸 데이터의 키를 보고 짐작한다. 값이 빈 칸은 안 건드린다.


8.3.6 맵에서 키 자체를 지우기 — null 과 삭제는 다르다

먼저 장면부터

번호표 사물함이 있어요. 2번 칸에 펜이 들어 있어요.

"2번 칸 물건을 바꿔줘" → 새 물건을 넣으면 돼요. (값 교체) "2번 칸 자체를 없애줘" → 칸을 통째로 빼야 하는데... 이게 까다로워요.

왜냐면 JSON에는 "없음(undefined)"을 적을 방법이 없거든요.

핵심 문제 — 세 가지 상태

일반 칸은 상태가 둘이에요.

값이 있음  → { "title": "멋진 방" }
값이 null → { "title": null }

그런데 맵(번호표 사물함)은 상태가 셋이에요.

값이 있음   → { "test": "value" }   ← 키 있음 + 값 있음
값이 null  → { "test": null }      ← 키 있음 + 값은 null
키 자체 없음 → {}                   ← 키가 아예 없음

이 세 번째 "키 자체 없음"을 JSON으로 표현하기가 어려워요.

해결법 — 마스크에 키를 적고, 본문에선 그 값을 빼기

// settings 맵에서 "test" 키를 통째로 지우고 싶을 때
PATCH /chatRooms/1?fieldMask=settings.`test`
Content-Type: application/json

{}

마스크엔 settings.test가 있는데 본문엔 그 값이 없어요. 서버는 "이 키를 바꿔야 하는데 값이 없네 = 없애라는 뜻이구나" 하고 키를 지워요.

잘못된 예 vs 올바른 예 (null 설정 vs 삭제)

settings = {"test": "value", "mode": "dark"}

# null로 설정 — 키는 남고 값만 None
settings["test"] = None        # {"test": None, "mode": "dark"}

# 삭제 — 키 자체가 사라짐
del settings["test"]           # {"mode": "dark"}
// API로 표현하면
값을 null로:  { "settings": { "test": null } }        → 키 남고 값만 null
키를 삭제:    {} + fieldMask=settings.`test`           → 키 자체 사라짐

비유-코드-위험 한눈에

비유 API/코드 위험·주의
사물함 칸에 빈 봉투 넣기 값을 null로 설정 칸(키)은 그대로 남음
사물함 칸을 통째로 떼기 fieldMask=settings.\test`+{}` 본문에 값을 빼야 삭제로 해석됨

핵심 한 줄. 맵은 "값 있음 / null / 키 없음" 세 상태가 있다. 키 삭제는 마스크에 키를 적고 본문은 비워서 보낸다.


8.3.7 없는 칸을 달라고 하면 — 에러 내지 말고 무시

먼저 장면부터

손님이 주문해요. "스테이크, 파스타, 그리고 '우주먹이' 주세요."

셰프가 어떻게 하는 게 좋을까요?

나쁜 대응: "우주먹이가 뭡니까?! 주문 전체 거부!" 좋은 대응: "우주먹이는 없으니 스테이크와 파스타만 드릴게요."

API도 좋은 셰프처럼 굴어야 해요.

표준 규칙(기본 안전판): 마스크에 없는 칸 이름이 들어와도 에러 내지 말고 그 칸은 그냥 무시한다.

왜 에러를 내면 안 되나

API는 시간이 가면서 칸이 생기고 사라져요. 예전엔 있던 칸이 지금은 없을 수 있어요. 이럴 때 에러를 내면 옛날 앱이 죄다 멈춰버려요(하위 호환성 깨짐).

그래서 이렇게 다뤄요. - 모든 구조를 유연하게(동적으로) 취급한다. - 마스크에 적힌 칸이 리소스에 없어도 에러 없이 넘어간다. - 없는 칸은 "없음(undefined)"으로 보고 그냥 무시한다.

비유-코드-위험 한눈에

비유 API/코드 위험·주의
메뉴에 없는 '우주먹이' 주문 없는 칸 이름이 마스크에 있음 에러 내면 옛날 앱이 다 멈춤
없는 건 빼고 나머지만 내줌 없는 칸은 무시하고 진행 하위 호환성 지킴

핵심 한 줄. 없는 칸을 달라고 해도 에러 대신 조용히 무시한다. 그래야 옛날 앱이 안 깨진다.


8.3.8 완성된 API 모양

지금까지 만든 걸 한 덩어리로 모으면 이래요.

abstract class ChatRoomApi {
  @get("/{id=chatRooms/*}")
  GetChatRoom(req: GetChatRoomRequest): ChatRoom;     // 부분 취득

  @patch("/{resource.id=chatRooms/*}")
  UpdateChatRoom(req: UpdateChatRoomRequest): ChatRoom; // 부분 업데이트
}

type FieldMask = string[];

interface GetChatRoomRequest {
  id: string;
  fieldMask: FieldMask;     // 받을 때 "이것만 줘"
}

interface UpdateChatRoomRequest {
  resource: ChatRoom;
  fieldMask: FieldMask;     // 바꿀 때 "이것만 바꿔"
}

replace 표준 메서드(Standard Replace): 리소스를 통째로 교체하는 HTTP PUT 방식. update 표준 메서드(Standard Update): 리소스 일부만 고치는 HTTP PATCH 방식. (둘 다 7장 표준 메서드의 일부예요. 지금은 "PUT=통째, PATCH=일부" 정도만 기억하면 돼요.)

핵심 한 줄. get 요청과 update 요청에 fieldMask 한 칸을 더하면, 부분 취득과 부분 업데이트가 둘 다 된다.


8.4 언제 쓰고 언제 안 쓰나 (트레이드오프)

표준 규칙부터 단순하게 정리하고, 예외는 박스로 미룰게요.

받을 때 — 전체 취득 vs 부분 취득

항목 전체 취득(기본) 부분 취득(마스크)
구현 난이도 단순 필터링 로직 필요
네트워크 비용 큰 리소스면 높음 낮음
어울리는 곳 칸이 적은 리소스 IoT, 모바일, 대용량

바꿀 때 — 전체 교체 vs 부분 업데이트

항목 전체 교체(PUT) 부분 업데이트(PATCH)
데이터 안전 동시 수정 시 유실 위험 안전(지정 칸만 변경)
하위 호환 새 칸 추가 시 위험 안전(모르는 칸 무시)
어울리는 곳 전체를 다 알 때 특정 칸만 바꿀 때

마스크 적는 법 — 명시적 vs 묵시적

항목 명시적 묵시적
정확도 100% 대부분 맞음
편의성 매번 적어야 함 자동 짐작
null 처리 명확 모호(null이 '삭제'인지 '무시'인지)
추천 상황 정확도 중요한 데이터 간단한 수정, 프로토타입

한 걸음 더 ▸ 모든 API에 넣어야 하나 (보편 지원)

한 걸음 더 ▸ 부분 업데이트는 거의 항상 가치 있어요(동시 수정 문제를 풀어주니까). 부분 취득은 선택이에요(리소스가 작으면 이득이 작아요).

단, 부분 취득을 지원하기로 했다면 모든 리소스에 똑같이 넣어야 해요. 일부 리소스만 지원하면 get 동작이 들쭉날쭉해져서 오히려 헷갈리는 API가 돼요.

한 걸음 더 ▸ 필드 마스크 말고 다른 방법

한 걸음 더 ▸ 부분 수정에는 다른 표준들도 있어요. 지금은 "이런 게 있다" 정도만.

대안 한 줄 설명 장단점
JSON Patch (RFC 6902) add·remove·replace 같은 연산 목록으로 수정 정교하지만 복잡
JSON Merge Patch (RFC 7396) 묵시적 마스크와 비슷, JSON 기반 동적 구조에서 null/없음 구분이 어려움
GraphQL 쿼리 언어로 원하는 칸을 정확히 지정 강력하지만 별도 생태계 필요

필드 마스크는 대부분 빠르고 간단하면서도 충분히 강력해요.


8.5 연습문제 — 직접 적용해 보기

worked example → 부분 완성 → 독립 적용 순서로 풀어볼게요.

문제 1 (worked example: 다 같이 풀어요)

채팅방이 관리자 여러 명을 담고 있어요. 이걸 배열로 저장하면 단점이 뭘까요? 어떻게 대체할까요?

배열의 단점 - 한 명만 수정 불가 — 번호가 불안정해서 "3번 관리자"가 누구인지 못 믿어요(8.3.3 줄 서기 비유). - 순서 보장 어려움 — 다른 사람이 추가/삭제하면 번호가 밀려요. - "Alice라는 관리자"를 콕 집어 가져올 수 없어요(번호로만 접근).

대안 1 — 맵으로 저장 (키 = 관리자 고유 ID)

interface ChatRoom {
  id: string;
  administrators: { [userId: string]: User };
}
// PATCH /chatRooms/1?fieldMask=administrators.`user_123`.email
// { "administrators": { "user_123": { "email": "[email protected]" } } }
// → user_123 관리자의 이메일만 콕 집어 수정!

대안 2 — 서브리소스로 분리 (각자 고유 주소)

GET    /chatRooms/1/administrators/user_123   → 한 명 조회
PATCH  /chatRooms/1/administrators/user_123   → 한 명 수정
DELETE /chatRooms/1/administrators/user_123   → 한 명 삭제

문제 2 (부분 완성: 빈칸을 채워보세요)

동적 맵에서 키 하나를 통째로 지우려면?

JSON에는 ____ 를 적을 방법이 없어서 키 삭제가 까다롭다.
방법: 필드 마스크에 지울 키를 적고, 요청 본문에선 그 값을 ____ 보낸다.

PATCH /chatRooms/1?fieldMask=settings.`test`
{}    ← 본문에 settings.test 값 없음 → 서버가 키를 삭제

빈칸 1: 없음(undefined)
빈칸 2: 빼고(생략하고)

(주의: { "settings": { "test": null } }는 키를 안 지워요. 값만 null이 돼요.)

문제 3 (독립 적용)

특정 칸만 부분 취득하겠다는 걸 어떻게 전달할까요?

쿼리 매개변수로 전달해요.

GET /chatRooms/1?fieldMask=title&fieldMask=description

이유: GET은 본문이 없고(HTTP 규칙), 헤더는 숨어서 디버깅이 어려워요. URL에 적으면 브라우저에서도 바로 보여요(8.3.1).

문제 4 (독립 적용)

모든 칸을 다루고 싶은데 칸 이름을 일일이 안 적으려면?

별표 * 센티넬 값을 써요.

GET /chatRooms/1?fieldMask=*   → "지금 있는 모든 칸을 줘"

칸 이름을 하드코딩하면 새로 생긴 칸을 놓쳐요. *는 새 칸도 자동 포함하고, 기본에서 빼둔 무거운 칸까지 포함할 수 있어요(8.3.4).

문제 5 (독립 적용)

settings 맵에서 "hello.world" 키를 바꾸려면 마스크를 어떻게 적을까요?

키 이름에 점(.)이 있으니 백틱으로 감싸요.

// 잘못: fieldMask=settings.hello.world → 3단계 중첩으로 오해
// 올바름: fieldMask=settings.`hello.world` → "hello.world" 키 하나
PATCH /chatRooms/1?fieldMask=settings.`hello.world`
{ "settings": { "hello.world": "new value" } }

한눈에 정리

문제: 통째로 주고받으면 낭비·충돌·유실
        ↓
해결: 필드 마스크 = "다룰 칸 이름 목록"
        ↓
   ┌──────────────┬───────────────┐
   │ 부분 취득     │ 부분 업데이트  │
   │ GET + 마스크 │ PATCH + 마스크 │
   └──────┬───────┴──────┬────────┘
          └── 둘 다 쿼리 매개변수로 전달 ──┘
        ↓
   칸 경로 규칙:  . = 안쪽 / * = 그 아래 전부 / ` = 특수 맵 키
   배열 = 통째 교체만
        ↓
   안 보내면:  GET = 전부 / PATCH = 짐작(묵시적)
   없는 칸 요청 = 에러 말고 무시
   맵 키 삭제 = 마스크에 키 적고 본문 비우기

더 알아볼 거리

이 노트는 원서 8장 범위 안에서만 정리했어요. 더 보고 싶으면 아래를 찾아보세요. - API Design Patterns (JJ Geewax, Manning) — 이 노트의 원본 교재 8장 - Google AIP-134 Standard Update — 부분 업데이트 가이드 - Google AIP-157 Partial Response — 부분 응답 가이드 - JSON Merge Patch (RFC 7396), JSON Patch (RFC 6902) — 부분 수정 표준 - Google Protobuf FieldMask — 필드 마스크의 원조 구현

다음 장 예고: 9장에서는 리소스를 한꺼번에 여러 개 다루는 방법을 봐요. 지금 몰라도 됩니다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플